Skip to main content

4.4 - Managing Groups- The Units Collection

A single Unit is an atom, but StarCraft II is a game of armies. To manage groups of units—your entire worker line, your main army, or an enemy attack wave—you need a more powerful tool. The Units object is that tool.

It is a specialized collection of Unit objects, supercharged with methods designed specifically for the logic of an RTS. Mastering the Units collection is the key to writing clean, efficient, and readable code for managing groups.

The Power of a Fluent API: Chaining Queries

The Units object uses a fluent API, which means that most of its filtering methods return a new Units object. This allows you to chain methods together, building a highly specific query from left to right. It is a far more powerful and readable alternative to nested loops and if statements.

[ self.units ] -> A huge collection of all your units.
|
v
[ .of_type(MARINE) ] -> Only Marines.
|
v
[ .idle ] -> Only idle Marines.
|
v
[ .sorted(key) ] -> Only idle Marines, sorted by distance.
|
v
[ .first ] -> The single, closest, idle Marine.

This elegant, single line of code replaces what would otherwise be a complex and error-prone block of loops.


Key Units Methods: A Developer's Toolkit

These methods can be grouped by their function.

CategoryMethodWhat It ReturnsPrimary Use Case in a Project
Checking.exists
.amount
bool
int
The most common checks: "Do I have any marines?" (if marines.exists:).
Selecting.first
.random
.random_or(None)
Unit
Unit
Unit or None
Grabbing a single unit from a collection. .random_or(None) is the safest option.
Filtering.of_type(type_id)
.idle
.ready
.closer_than(dist, pos)
.further_than(dist, pos)
UnitsThese are the core of the fluent API. They narrow down your collection to only the units that match a specific criteria.
Ordering & Finding.sorted(key)
.closest_to(pos)
.furthest_to(pos)
.center
Units
Unit
Unit
Point2
Used after filtering to find the best candidate from a group, or to find the group's center of mass.

A Developer's Checklist for Managing a Group

When you need to command a group of units, follow this process:

  • 1. Start Broad: Get the initial collection (e.g., self.units).
  • 2. Filter by Type: Narrow it down to the units you care about (e.g., self.units(UnitTypeId.MARINE)).
  • 3. Filter by State: Apply state-based filters (e.g., .idle, .ready).
  • 4. Check for Existence: Always check .exists before trying to access a unit from the collection.
  • 5. Select or Iterate: Grab a single unit (.closest_to()) or loop through the entire filtered collection to issue commands.

Code Example: The Army Commander Bot

This bot demonstrates advanced use of Units collections to manage an army. It will group idle units into a control group and send them to attack, while also pulling back damaged units from the front line.

# army_commander_bot.py

from sc2.main import run_game
from sc2 import maps
from sc2.bot_ai import BotAI
from sc2.ids.unit_typeid import UnitTypeId
from sc2.player import Bot, Computer
from sc2.data import Difficulty, Race
from sc2.units import Units


class ArmyCommanderBot(BotAI):
"""Demonstrates advanced group management with the Units collection."""

async def on_step(self, iteration: int):
# We need a target to attack.
attack_target = self.enemy_start_locations[0]
if self.enemy_structures.exists:
attack_target = self.enemy_structures.random.position

# 1. Manage the main army.
await self.manage_army(attack_target)

# 2. Keep building units to reinforce the army.
await self.train_reinforcements()

async def manage_army(self, attack_target):
# Create a Units collection of all our marines.
marines: Units = self.units(UnitTypeId.MARINE)

# Use chained queries to form two distinct groups.
# Group 1: Healthy marines that are idle and ready to attack.
idle_and_healthy_marines: Units = marines.idle.filter(
lambda m: m.health_percentage > 0.8
)

# Group 2: Any marine that is damaged, regardless of what it's doing.
damaged_marines: Units = marines.filter(lambda m: m.health_percentage <= 0.8)

# Command the idle and healthy group >=10 marines to attack the target.
if idle_and_healthy_marines.exists and idle_and_healthy_marines.amount >= 10:
# We issue a command to the entire Units collection.
for idle_and_healthy_marine in idle_and_healthy_marines:
idle_and_healthy_marine.attack(attack_target)

# Command the damaged group to retreat to a safe rally point.
if damaged_marines.exists:
safe_rally_point = self.start_location.towards(
self.game_info.map_center, 15
)
for damaged_marine in damaged_marines:
damaged_marine.move(safe_rally_point)

async def train_reinforcements(self):
"""A simple method to keep building marines."""
# Find idle, ready barracks.
idle_barracks: Units = self.structures(UnitTypeId.BARRACKS).ready.idle
if self.can_afford(UnitTypeId.MARINE) and idle_barracks.exists:
# Train a marine from a random idle barracks.
idle_barracks.random.train(UnitTypeId.MARINE)

# Build supply depot.
elif self.structures(UnitTypeId.SUPPLYDEPOT).amount < 2 and self.can_afford(
UnitTypeId.SUPPLYDEPOT
):
await self.build(
UnitTypeId.SUPPLYDEPOT,
near=self.start_location.towards(self.game_info.map_center, 5),
)

# Build barracks.
elif not self.structures(UnitTypeId.BARRACKS).exists and self.can_afford(
UnitTypeId.BARRACKS
):
await self.build(
UnitTypeId.BARRACKS,
near=self.start_location.towards(self.game_info.map_center, 8),
)


if __name__ == "__main__":
run_game(
maps.get("AbyssalReefLE"),
[Bot(Race.Terran, ArmyCommanderBot()), Computer(Race.Zerg, Difficulty.Easy)],
realtime=False,
)